Redis 各种数据结构的使用场景
String 的使用场景
String 的内部编码
Redis会根据当前值的类型和长度决定使用哪种内部编码来实现。
字符串类型的内部编码有3种:
- int:8 个字节的长整型。
- embstr:小于等于 39 个字节的字符串。
- raw:大于 39 个字节的字符串。
数据库缓存
在 web 服务中,使用 MySQL 作为数据库,Redis作为缓存。
由于 Redis 具有支撑高并发的特性,通常能起到加速读写和降低后端压力的作用。web 端的大多数请求都是从 Redis 中获取的数据,如果 Redis 中没有需要的数据,则会从 MySQL 中去获取,并将获取到的数据写入 Redis。
阅读量,播放量计数
Redis 中有一个字符串相关的命令 incr key,incr 命令对值做自增操作,返回结果分为以下三种情况:
- 值不是整数,返回错误
- 值是整数,返回自增后的结果
- key 不存在,默认键为 0,返回 1(就是直接新键一个 key)
比如文章的阅读量,视频的播放量等等都会使用 Redis 来计数,每播放一次,对应的播放量就会加 1,同时将这些数据异步存储到数据库中达到持久化的目的。
共享 Session
在分布式系统中,用户的每次请求会访问到不同的服务器,这就会导致 Session 不同步的问题,假如一个用来获取用户信息的请求落在A 服务器上,获取到用户信息后存入 Session。
下一个请求落在 B 服务器上,想要从 Session 中获取用户信息就不能正常获取了,因为用户信息的 Session 在服务器A上,为了解决这个问题,使用 Redis 集中管理这些 Session,将 Session 存入 Redis,使用的时候直接从 Redis中获取就可以了。
IP 限速
为了安全考虑,有些网站会对 IP 进行限制,限制同一 IP 在一定时间内访问次数不能超过 n 次。
哈希 Hash 的使用场景
Redis 中,哈希类型是指一个键值对的存储结构。
# 储存语法
hset key field value
# Redis 的 Hash 可以同时插入多个数据
hset key field01 value01 field02 value02 field03 value03
# 获取
hget key field
# 获取所有列
hgetall key
# 删除
hdel key field
维护一个简单的数据表
由于 hash 类型存储的是一个键值对,比如数据库有以下一个用户表结构
id | name | age |
---|
将以上信息存入 redis,用表明 user:id
作为 key,用户属性作为值:
# hset 可以同时插入多个字段
# 语法:
hset key field01 value01 field02 value02 field03 value03
# 使用如下(注意,这里的 user:1 整个为一个 key):
hset user:1 name 张三 age 18
使用哈希存储会比字符串更加方便直观
这种数据表只适合索引查询,不适合范围查询(Hash 也没这功能)
装载聚合对象
Hash 可以用于表示对象的属性,每个字段表示对象的一个属性,值表示该属性的值。
像一些聚合对象可以丢到 Redis 中,通过 Hash 来存储,这样可以很方便的只取得对象某些信息,而不用取出整个对象。(巨无霸聚合根)
列表 List 使用场景
列表类型用来存储多个有序的字符串,一个列表最多可以存储 2^32-1 个元素,列表的两端都可以插入和弹出元素。
List 的内部编码
列表的内部编码有两种:
ziplist(压缩列表):当哈希类型元素个数小于 list-max-ziplist-entries 配置(默认512个)同时所有值都小于 list-max-ziplist-value 配置(默认64字节)时使用。这个 ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以比 hashtable 更加节省内存。
linkedlist(链表):当 ziplist 不能满足要求时,会使用 linkedlist。
消息队列
列表用来存储多个有序的字符串,既然是有序的,那么就满足消息队列的特点。使用 lpush + rpop 或者 rpush + lpop 实现消息队列。
除此之外,Redis 支持阻塞操作,在弹出元素的时候使用阻塞命令来实现阻塞队列(主要就是使用阻塞队列)。
BLPOP 是阻塞式列表的弹出原语。 它是命令 LPOP 的阻塞版本,这是因为当给定列表内没有任何元素可供弹出的时候, 连接将被 BLPOP 命令阻塞。
直到有另一个客户端对给定的这些 key 的任意一个执行 LPUSH 或 RPUSH 命令为止。 当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素
返回值:返回 key 和弹出的元素值
超时时返回 nil
有时候,为了等待一个新元素到达数据中,需要使用轮询的方式对数据进行探查。
另一种更好的方式是,使用系统提供的阻塞原语,在新元素到达时立即进行处理,而新元素还没到达时,就一直阻塞住,避免轮询占用资源。BLPOP 和 BRPOP 能很好解决这个问题。
和 Java 提供的阻塞队列如 LinkedBlockingQueue 相比,Redis 提供读取阻塞,不提供写入阻塞。
在 Spring 中使用 Redis,实现线程阻塞
//list集合 第一个元素为key值,第二个元素为弹出的元素值;当超时返回[null]
List<Object> obj = redisTemplate.executePipelined(new RedisCallback<Object>() {
@Nullable
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
//队列没有元素会阻塞操作,直到队列获取新的元素或超时
return connection.bLPop(timeout, key.getBytes());
}
}, new StringRedisSerializer());
我们通过向队列添加元素来释放阻塞的线程
redisTemplate.opsForList().rightPush(key,value);
维护一个栈
由于列表存储的是有序字符串,满足队列的特点,也就能满足栈先进后出的特点,使用 lpush + lpop 或者 rpush + rpop 实现栈。
分页获取文章列表
因为列表的元素不但是有序的,而且还支持按照索引范围获取元素。因此我们可以使用命令 lrange key 0 9
分页获取文章列表
集合 Set 的使用场景
集合类型也可以保存多个字符串元素,与列表不同的是,集合中不允许有重复元素并且集合中的元素是无序的。一个集合最多可以存储 2^32-1 个元素。
Set 的内部编码
集合类型的内部编码有两种:
intset(整数集合):当集合中的元素都是整数且元素个数小于 set-max-intset-entries 配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用。
hashtable(哈希表):当 intset 不能满足要求时,会使用 hashtable。
用户标签 Tag(交并集)
例如一个用户对篮球、足球感兴趣,另一个用户对橄榄球、乒乓球感兴趣,这些兴趣点就是一个标签。
有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同感兴趣的标签。
给用户打标签的时候需要:
- 给用户打标签,
- 给标签加用户。
注意:需要给这两个操作增加事务
给用户打标签
sadd user:1:tags tag1 tag2
给标签添加用户
sadd tag1:users user:1
sadd tag2:users user:1
使用交集(sinter)求两个 user 的共同标签
sinter user:1:tags user:2:tags
抽奖功能(随机取列表)
集合有两个命令支持获取随机数,分别是:
随机获取 count 个元素,集合元素个数不变
srandmember key [count]
随机弹出 count 个元素,元素从集合弹出,集合元素个数改变
spop key [count]
用户点击抽奖按钮,参数抽奖,将用户编号放入集合,然后抽奖,分别抽一等奖、二等奖,如果已经抽中一等奖的用户不能参数抽二等奖则使用 spop,反之使用 srandmember。
有序集合 ZSet(Sorted Set)
有序集合和集合一样,不能有重复元素。但是可以排序,它给每个元素设置一个 Score 作为排序的依据。最多可以存储 2^32-1 个元素。
它还可以用于取交集、并集、差集(和上面的 Set 集合是一样的)。
ZSet 内部编码
有序集合类型的内部编码有两种:
1、ziplist(压缩列表):当有序集合的元素个数小于list-max-ziplist-entries配置(默认128个)同时所有值都小于list-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,更加节省内存。
2、skiplist(跳跃表):当不满足 ziplist 的要求时,会使用 skiplist。
具体数据结构可以看 “Redis 各种数据结构的内部原理” 那篇文章。
排行榜
用户发布了 n 篇文章,其他人看到文章后给喜欢的文章点赞,使用 score 来记录点赞数,有序集合会根据 score 排行。(具体细节看 Redis 实战那篇文章)
流程如下:
用户发布一篇文章,初始点赞数为 0,即 score 为 0
zadd user:article 0 a
有人给文章 a 点赞,递增 1
zincrby user:article 1 a
查询点赞前三篇文章
zrevrange user:article 0 2
查询点赞后三篇文章
zrange user:article 0 2
延迟消息队列
下单系统,下单后需要在 15 分钟内进行支付,如果 15 分钟未支付则自动取消订单。
将下单后的十五分钟后时间作为 score,订单作为 value 存入 redis,消费者轮询去消费,如果消费的大于等于这笔记录的 score,则将这笔记录移除队列,取消订单。
查询价格的范围
ZSET 提供了根据分数范围进行查询的功能。例如,可以通过指定最小和最大分数来获取某个分数范围内的成员列表,这对于实现区间查询非常有用。
假设我们有一个有序集合,存储了不同商品的价格信息,并且每个商品的价格作为有序集合的分数。我们想要查询价格在某个范围内的商品列表。
示例命令如下:
ZADD products 10 "Product A"
ZADD products 15 "Product B"
ZADD products 20 "Product C"
ZADD products 25 "Product D"
ZADD products 30 "Product E"
ZRANGEBYSCORE products 15 25
以上命令执行了以下操作:
- 使用
ZADD
命令向有序集合products
中添加商品和对应的价格。每个商品的价格作为有序集合的分数。 - 使用
ZRANGEBYSCORE
命令查询有序集合products
中价格在 15 到 25 之间的商品列表。参数15和25分别表示范围的最小值和最大值。
执行以上命令后,ZRANGEBYSCORE
命令将返回价格在 15 到 25 之间的商品列表,按照价格从低到高排序。你可以根据需要进行后续处理或展示查询结果。
需要注意的是,ZRANGEBYSCORE
命令还提供了一些可选参数,例如指定返回结果的数量、限定结果的偏移量等,以满足不同的查询需求。
事件调度和过期处理
ZSET 的分数可以表示时间戳或时间窗口,可以使用 ZSET 来管理和调度基于时间的事件。通过添加事件的时间戳作为分数,可以使用 ZSET 的有序性质获取过期事件或按时间顺序执行事件。
假设我们有一个有序集合,存储了一些事件的时间戳,并且每个事件的时间戳作为有序集合的分数。我们希望使用ZSET来进行事件调度和过期处理,即根据事件的时间戳来获取过期事件或按时间顺序执行事件。
示例命令如下:
ZADD events 1622880000 "Event A"
ZADD events 1622890000 "Event B"
ZADD events 1622900000 "Event C"
ZADD events 1622910000 "Event D"
ZADD events 1622920000 "Event E"
ZREMRANGEBYSCORE events 0 1622900000
ZRANGE events 0 -1
以上命令执行了以下操作:
- 使用
ZADD
命令向有序集合events
中添加事件和对应的时间戳。每个事件的时间戳作为有序集合的分数。 - 使用
ZREMRANGEBYSCORE
命令删除时间戳在 0 到 1622900000 之间的事件,即删除过期的事件。参数0和1622900000分别表示范围的最小值和最大值。 - 使用
ZRANGE
命令获取有序集合events
中的所有事件,通过指定范围 0 -1 获取全部事件。
执行以上命令后,ZREMRANGEBYSCORE
命令将删除过期的事件,而 ZRANGE
命令将返回剩余的未过期事件列表,按照时间戳从低到高排序。你可以根据需要进行后续处理或执行相应的操作。
基数统计 HyperLogLog
HyperLogLog 数据结构在以下场景中非常有用:
唯一用户统计:HyperLogLog 可以用于估算独立用户的数量,例如网站的访问者数量、应用程序的用户数量等。通过将用户的标识作为元素添加到 HyperLogLog 中,然后使用
PFCOUNT
命令获取基数估算值,可以快速估算独立用户的数量,而不需要存储和维护每个用户的具体信息。页面点击量统计:HyperLogLog 可以用于估算页面点击量(Pageviews)的数量。通过将用户的访问标识(如 IP 地址或用户 ID)作为元素添加到 HyperLogLog 中,然后使用
PFCOUNT
命令获取基数估算值,可以得到页面的独立访问数量。社交媒体分享统计:HyperLogLog 可以用于估算某个内容在社交媒体上的分享次数。通过将用户的分享标识(如分享 ID)作为元素添加到 HyperLogLog 中,然后使用
PFCOUNT
命令获取基数估算值,可以快速估算内容的独立分享次数。广告点击量统计:HyperLogLog 可以用于估算广告的点击次数。通过将用户的点击标识(如点击 ID)作为元素添加到 HyperLogLog 中,然后使用
PFCOUNT
命令获取基数估算值,可以快速估算广告的独立点击次数。
需要注意的是,HyperLogLog 是一种概率性数据结构,估算的基数值是近似值,并且可能存在一定的误差。误差的范围通常受到位图大小和元素数量的影响。在实际使用中,可以根据需求设置合适的位图大小以平衡内存占用和估算精度。
当使用 HyperLogLog 进行基数统计时,以下是一个具体的使用场景例子,包含对应的 Redis 命令:
估算每天独立用户的数量
假设你有一个在线论坛,你想要估算每天独立用户的数量。你可以使用 HyperLogLog 数据结构来进行基数统计。
用户登录:
每当用户登录到论坛时,你可以使用
PFADD
命令将用户的标识添加到 HyperLogLog 中。例如:PFADD daily_users_set user1
PFADD daily_users_set user2
PFADD daily_users_set user3这将将用户标识 "user1"、"user2" 和 "user3" 添加到名为 "daily_users_set" 的 HyperLogLog 中。
获取基数估算值:
使用
PFCOUNT
命令可以获取 HyperLogLog 中的基数估算值,即独立用户的数量。例如:PFCOUNT daily_users_set
这将返回一个近似的用户数量。
重置 HyperLogLog:
每天结束后,你可能希望重置 HyperLogLog,以便进行下一天的统计。使用
PFDEL
命令可以删除 HyperLogLog。例如:PFDEL daily_users_set
这将删除名为 "daily_users_set" 的 HyperLogLog。
通过上述流程,你可以每天使用 HyperLogLog 进行独立用户数量的估算。请注意,基于 HyperLogLog 的基数估算是近似值,但通常能够提供相对准确的结果,同时在占用较小内存的情况下进行高效计数。
哪些结构时间复杂度较高
Redis 是一个高效的内存键值存储数据库,它通过优化的数据结构和算法来实现高性能。大多数 Redis 操作的时间复杂度都是 O(1)(常数时间复杂度),这意味着无论数据规模的大小,操作所需的时间都是固定的。
然而,有一些特殊的 Redis 数据结构或操作可能具有较高的时间复杂度,取决于具体的使用情况。以下是一些在特定情况下可能具有较高时间复杂度的 Redis 结构和操作:
集合(Set)和有序集合(Sorted Set)的插入和删除操作:对于较大的集合和有序集合,插入和删除操作的时间复杂度可能会随着数据规模的增加而增加,最坏情况下可以达到 O(N),其中 N 是集合或有序集合中的元素数量。
集合(Set)和有序集合(Sorted Set)的交集、并集、差集操作:当进行多个集合或有序集合之间的交集、并集或差集操作时,操作的时间复杂度将取决于参与操作的集合的大小,最坏情况下可能达到 O(N*M),其中 N 和 M 分别是参与操作的集合的大小。
列表(List)的插入和删除操作:当在列表的两端进行插入和删除操作时,时间复杂度是 O(1)。但如果在列表的中间位置进行插入和删除操作,将需要移动和调整元素的位置,时间复杂度将变为 O(N),其中 N 是列表的长度。
需要根据具体的使用场景和操作需求来评估数据结构的时间复杂度,并根据性能要求进行适当的优化和调整。大多数情况下,Redis 的操作时间复杂度仍然非常高效,并能够满足大部分的实际需求。